package com.free.utils;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;

/**
 * 身份证号码关联共通方法
 * 1、公民身份号码是特征组合码，由十七位数字本体码和一位校验码组成。排列顺序从左至右依次为：
 *      六位数字地址码，八位数字出生日期码，三位数字顺序码和一位数字校验码。
 * 2、地址码（前六位数）
 *      表示编码对象常住户口所在县(市、旗、区)的行政区划代码，按GB/T2260的规定执行。
 * 3、出生日期码（第七位至十四位）
 *      表示编码对象出生的年、月、日，按GB/T7408的规定执行，年、月、日代码之间不用分隔符。
 * 4、顺序码（第十五位至十七位）
 *      表示在同一地址码所标识的区域范围内，对同年、同月、同日出生的人编定的顺序号， 顺序码的奇数分配给男性，偶数分配给女性。
 * 5、校验码（第十八位数）
 *     （1）先对前17位数字的权求和。十七位数字本体码加权求和公式：
 *              S = Sum(Ai * Wi), i = 0, ... , 16
 *              Ai：表示第i位置上的身份证号码数字值
 *              Wi：表示第i位置上的加权因子 Wi: 7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2
 *     （2）计算模 Y = mod(S, 11)
 *     （3）通过模得到对应的校验码 Y: 0 1 2 3 4 5 6 7 8 9 10 校验码: 1 0 X 9 8 7 6 5 4 3 2
 */
public class IDCardUtils {

    /*========================================================================*
     *                         Public Fields (公共属性)
     *========================================================================*/

    /*========================================================================*
     *                         Private Fields (私有属性)
     *========================================================================*/

    // 正确的校验码
    private static final String[] CHECK_CODE = {"1", "0", "x", "9", "8", "7", "6", "5", "4", "3", "2"};

    // 加权因子
    private static final int[] WEIGHT_FACTOR = {7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2};


    // 有效地区码
    private static final List<String> AREA_CODE = Arrays.asList(
            "11", // 北京
            "12", // 天津
            "13", // 河北
            "14", // 山西
            "15", // 内蒙古
            "21", // 辽宁
            "22", // 吉林
            "23", // 黑龙江
            "31", // 上海
            "32", // 江苏
            "33", // 浙江
            "34", // 安徽
            "35", // 福建
            "36", // 江西
            "37", // 山东
            "41", // 河南
            "42", // 湖北
            "43", // 湖南
            "44", // 广东
            "45", // 广西
            "46", // 海南
            "50", // 重庆
            "51", // 四川
            "52", // 贵州
            "53", // 云南
            "54", // 西藏
            "61", // 陕西
            "62", // 甘肃
            "63", // 青海
            "64", // 宁夏
            "65", // 新疆
            "71", // 台湾
            "81", // 香港
            "82", // 澳门
            "91" // 国外
    );

    /*========================================================================*
     *                         Construct Methods (构造方法)
     *========================================================================*/

    /*========================================================================*
     *                         Public Methods (公有方法)
     *========================================================================*/

    /**
     * 身份证的有效验证
     *
     * @param
     *         cardId   身份证号
     *
     * @return
     *         0    正确的身份证号码
     *         1    长度不对（长度应该为 15 位或 18 位）
     *         2    格式不对（前 17 位为数字，最后一位为数字或者 x）
     *         3    出生年月无效（不是合法的日期）
     *         4    出生年月无效（出生年月在未来或者是 150 年前）
     *         5    无效地区码
     *         6    校验码不对
     */
    public static int checkIdCard(String cardId)
    {
        // 号码的长度是 15 位的话先补成 18 位
        String newCardId = oldCardIdToNewCardId(cardId);

        // 长度不对
        if (newCardId.length() != 18)
        {
            return 1;
        }

        // 格式不对（前 17 位为数字，最后一位为数字或者 x）
        if (!newCardId.matches("^[0-9]{17}[0-9xX]$"))
        {
            return 2;
        }

        LocalDate date = null;

        try
        {
            date = LocalDate.parse(newCardId.substring(6, 14), DateTimeFormatter.BASIC_ISO_DATE);
        }
        catch (DateTimeParseException e)
        {
            // 出生年月无效
            return 3;
        }

        long age = ChronoUnit.YEARS.between(date, LocalDate.now());

        // 出生年月在未来或者是 150 年前
        if (age <= 0 || age >= 150)
        {
            return 4;
        }

        // 无效地区码
        if (!AREA_CODE.contains(newCardId.substring(0, 2)))
        {
            return 5;
        }

        // 初始长度 15 位的场合就不检查校验码了
        if (cardId.length() == 15)
        {
            return 0;
        }

        // 检查校验码
        int checkSum = 0;
        for (int i = 0; i < 17; i++)
        {
            checkSum = checkSum + (newCardId.charAt(i) - '0') * WEIGHT_FACTOR[i];
        }

        String verifyCode = CHECK_CODE[checkSum % 11];

        if (!verifyCode.equalsIgnoreCase(newCardId.substring(17)))
        {
            return 6;
        }

        return 0;
    }


    /**
     * 根据身份证号码获取出生日期（只能在是合法身份证号码的场合下调用）
     * @param cardId 身份证号
     * @return 出生日期（yyyy-MM-dd）
     */
    public static String getBirthday(String cardId)
    {
        // 号码的长度是 15 位的话先补成 18 位
        String newCardId = oldCardIdToNewCardId(cardId);

        String year = newCardId.substring(6, 10);
        String month = newCardId.substring(10, 12);
        String day = newCardId.substring(12, 14);

        return String.join("-", year, month, day);
    }


    /**
     * 根据身份证号码获取性别（只能在是合法身份证号码的场合下调用）
     * @param cardId 身份证号
     * @return 0: 女性 1: 男性
     */
    public static int getSex(String cardId)
    {
        // 号码的长度是 15 位的话先补成 18 位
        String newCardId = oldCardIdToNewCardId(cardId);

        return Integer.parseInt(newCardId.substring(16, 17)) % 2;
    }


    /**
     * 根据身份证号码计算当前年龄（只能在是合法身份证号码的场合下调用）
     * @param cardId 身份证号
     * @return
     */
    public static int getAge(String cardId)
    {
        return (int) ChronoUnit.YEARS.between(LocalDate.parse(getBirthday(cardId), DateTimeFormatter.ofPattern("yyyy-MM-dd")), LocalDate.now());
    }

    /*========================================================================*
     *                         Private Methods (私有方法)
     *========================================================================*/

    // 身份证号码的长度是 15 位的话先补成 18 位
    private static String oldCardIdToNewCardId(String cardId)
    {
        if (cardId == null)
        {
            return "";
        }

        if (cardId.length() != 15)
        {
            return cardId;
        }

        return cardId.substring(0, 6) + "19" + cardId.substring(6, 15) + "x";
    }

}
